/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.sentry;

import io.sentry.protocol.User;

import java.util.Date;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * Session
 *
 * @since 2021-06-02
 */
public final class Session {
    /**
     * Session state
     *
     * @since 2021-06-02
     */
    public enum State {
        /**
         * Ok
         */
        Ok,
        /**
         * Exited
         */
        Exited,
        /**
         * Crashed
         */
        Crashed,
        /**
         * not currently used in this SDK.
         */
        Abnormal
    }

    /**
     * started timestamp
     */
    private final @NotNull Date started;

    /**
     * the timestamp
     */
    private @Nullable Date timestamp;

    /**
     * the number of errors on the session
     */
    private final @NotNull AtomicInteger errorCount;

    /**
     * The distinctId, did
     */
    private final @Nullable String distinctId;

    /**
     * the SessionId, sid
     */
    private final @Nullable UUID sessionId;

    /**
     * The session init flag
     */
    private @Nullable Boolean init;

    /**
     * The session state
     */
    private @NotNull State status;

    /**
     * The session sequence
     */
    private Long sequence;

    /**
     * The session duration (timestamp - started)
     */
    private @Nullable Double duration;

    /**
     * the user's ip address
     */
    private final @Nullable String ipAddress;

    /**
     * the user Agent
     */
    private @Nullable String userAgent;

    /**
     * the environment
     */
    private final @Nullable String environment;

    /**
     * the App's release
     */
    private final @NotNull String release;

    /**
     * The session lock, ops should be atomic
     */
    private final @NotNull Object sessionLock = new Object();

    /**
     * constructor
     *
     * @param status State
     * @param started Date
     * @param timestamp Date
     * @param errorCount int
     * @param distinctId String
     * @param sessionId UUID
     * @param init Boolean
     * @param sequence Long
     * @param duration Double
     * @param ipAddress String
     * @param userAgent String
     * @param environment String
     * @param release String
     */
    public Session(
        final @NotNull State status,
        final @NotNull Date started,
        final @Nullable Date timestamp,
        final int errorCount,
        final @Nullable String distinctId,
        final @Nullable UUID sessionId,
        final @Nullable Boolean init,
        final @Nullable Long sequence,
        final @Nullable Double duration,
        final @Nullable String ipAddress,
        final @Nullable String userAgent,
        final @Nullable String environment,
        final @NotNull String release) {
        this.status = status;
        this.started = (Date) started.clone();
        this.timestamp = (Date) timestamp.clone();
        this.errorCount = new AtomicInteger(errorCount);
        this.distinctId = distinctId;
        this.sessionId = sessionId;
        this.init = init;
        this.sequence = sequence;
        this.duration = duration;
        this.ipAddress = ipAddress;
        this.userAgent = userAgent;
        this.environment = environment;
        this.release = release;
    }

    /**
     * constructor
     *
     * @param distinctId String
     * @param user User
     * @param environment String
     * @param release String
     */
    public Session(
        @Nullable String distinctId,
        final @Nullable User user,
        final @Nullable String environment,
        final @NotNull String release) {
        this(
            State.Ok,
            DateUtils.getCurrentDateTime(),
            DateUtils.getCurrentDateTime(),
            0,
            distinctId,
            UUID.randomUUID(),
            true,
            null,
            null,
            user != null ? user.getIpAddress() : null,
            null,
            environment,
            release);
    }

    /**
     * get started date
     *
     * @return date
     */
    @SuppressWarnings({"JdkObsolete", "JavaUtilDate"})
    public @Nullable Date getStarted() {
        if (started == null) {
            return null;
        }
        return (Date) started.clone();
    }

    public @Nullable String getDistinctId() {
        return distinctId;
    }

    public @Nullable UUID getSessionId() {
        return sessionId;
    }

    public @Nullable String getIpAddress() {
        return ipAddress;
    }

    public @Nullable String getUserAgent() {
        return userAgent;
    }

    public @Nullable String getEnvironment() {
        return environment;
    }

    public @NotNull String getRelease() {
        return release;
    }

    public @Nullable Boolean getInit() {
        return init;
    }

    /**
     * Used for migrating the init flag when an session is gonna be deleted.
     */
    @ApiStatus.Internal
    public void setInitAsTrue() {
        this.init = true;
    }

    /**
     * error count
     *
     * @return errorCount
     */
    public int errorCount() {
        return errorCount.get();
    }

    public @NotNull State getStatus() {
        return status;
    }

    public @Nullable Long getSequence() {
        return sequence;
    }

    public @Nullable Double getDuration() {
        return duration;
    }

    /**
     * get timestamp
     *
     * @return date
     */
    @SuppressWarnings({"JdkObsolete", "JavaUtilDate"})
    public @Nullable Date getTimestamp() {
        final Date timestampRef = timestamp;
        return timestampRef != null ? (Date) timestampRef.clone() : null;
    }

    /**
     * Ends a session and update its values
     */
    public void end() {
        end(DateUtils.getCurrentDateTime());
    }

    /**
     * Ends a session and update its values
     *
     * @param date the timestamp or null
     */
    public void end(final @Nullable Date date) {
        synchronized (sessionLock) {
            init = null;

            // at this state it might be Crashed already, so we don't check for it.
            if (status == State.Ok) {
                status = State.Exited;
            }

            if (date != null) {
                this.timestamp = (Date) date.clone();
            } else {
                this.timestamp = DateUtils.getCurrentDateTime();
            }

            if (this.timestamp != null) {
                duration = calculateDurationTime(this.timestamp);
                sequence = getSequenceTimestamp(this.timestamp);
            }
        }
    }

    /**
     * Calculates the duration time in seconds timestamp (last update) - started
     *
     * @param date the timestamp
     * @return duration in seconds
     */
    @SuppressWarnings({"JdkObsolete", "JavaUtilDate"})
    private double calculateDurationTime(final @NotNull Date date) {
        long diff = Math.abs(date.getTime() - started.getTime());
        return (double) diff / 1000; // duration in seconds
    }

    /**
     * Updates the current session and set its values
     *
     * @param state the status
     * @param agent the userAgent
     * @param addErrorsCount true if should increase error count or not
     * @return if the session has been updated
     */
    public boolean update(
        final @Nullable State state, final @Nullable String agent, boolean addErrorsCount) {
        synchronized (sessionLock) {
            boolean sessionHasBeenUpdated = false;
            if (state != null) {
                this.status = state;
                sessionHasBeenUpdated = true;
            }

            if (agent != null) {
                this.userAgent = agent;
                sessionHasBeenUpdated = true;
            }
            if (addErrorsCount) {
                errorCount.addAndGet(1);
                sessionHasBeenUpdated = true;
            }

            if (sessionHasBeenUpdated) {
                init = null;
                timestamp = DateUtils.getCurrentDateTime();
                if (timestamp != null) {
                    sequence = getSequenceTimestamp(timestamp);
                }
            }
            return sessionHasBeenUpdated;
        }
    }

    /**
     * Returns a logical clock.
     *
     * @param date The timestamp
     * @return time stamp in milliseconds UTC
     */
    @SuppressWarnings({"JdkObsolete", "JavaUtilDate"})
    private long getSequenceTimestamp(final @NotNull Date date) {
        long dateTime = date.getTime();
        if (dateTime < 0) {
            dateTime = Math.abs(dateTime);
        }
        return dateTime;
    }

    /**
     * Ctor copy of the Session
     *
     * @return a copy of the Session
     */
    @SuppressWarnings("MissingOverride")
    public @NotNull Session cloneSession() {
        return new Session(
            status,
            started,
            timestamp,
            errorCount.get(),
            distinctId,
            sessionId,
            init,
            sequence,
            duration,
            ipAddress,
            userAgent,
            environment,
            release);
    }
}
